<template>
  <view class="lime-clipper" :class="{open: value}" disable-scroll :style="'z-index: ' + zIndex + ';' + customStyle">
    <view class="lime-clipper-mask" @touchstart.stop.prevent="clipTouchStart" @touchmove.stop.prevent="clipTouchMove"
      @touchend.stop.prevent="clipTouchEnd">
      <view class="lime-clipper__content" :style="clipStyle">
        <view class="lime-clipper__edge" v-for="(item, index) in [0, 0, 0, 0]" :key="index"></view>
      </view>
    </view>
    <image class="lime-clipper-image" @error="imageLoad" @load="imageLoad" @touchstart="imageTouchStart"
      @touchmove="imageTouchMove" @touchend="imageTouchEnd" :src="image"
      :mode="imageWidth == 'auto' ? 'widthFix' : 'scaleToFill'" v-if="image" :style="imageStyle" />
    <canvas canvas-id="lime-clipper" id="lime-clipper" disable-scroll
      :style="'width: ' + canvasWidth * scaleRatio + 'px; height:' + canvasHeight * scaleRatio + 'px;'"
      class="lime-clipper-canvas"></canvas>
    <view class="lime-clipper-tools">
      <view class="lime-clipper-tools__btns">
        <view v-if="isShowCancelBtn" @tap="cancel">
          <slot name="cancel" v-if="$slots.cancel" />
          <view v-else class="cancel">取消</view>
        </view>
        <view v-if="isShowPhotoBtn" @tap="uploadImage">
          <slot name="photo" v-if="$slots.photo" />
          <image v-else src="/uni_modules/lime-clipper/static/photo.svg" />
        </view>
        <view v-if="isShowRotateBtn" @tap="rotate">
          <slot name="rotate" v-if="$slots.rotate" />
          <image v-else src="/uni_modules/lime-clipper/static/rotate.svg" data-type="inverse" />
        </view>
        <view v-if="isShowConfirmBtn" @tap="confirm">
          <slot name="confirm" v-if="$slots.confirm" />
          <view v-else class="confirm">确定</view>
        </view>
      </view>
      <slot></slot>
    </view>
  </view>
</template>

<script>
  import {
    determineDirection,
    calcImageOffset,
    calcImageScale,
    calcImageSize,
    calcPythagoreanTheorem,
    clipTouchMoveOfCalculate,
    imageTouchMoveOfCalcOffset,
  } from './utils';
  const cache = {}
  export default {
    name: 'lime-clipper',
    props: {
      value: {
        type: Boolean,
        default: true
      },
      // #ifdef MP-WEIXIN
      type: {
        type: String,
        default: '2d'
      },
      // #endif
      customStyle: {
        type: String,
      },
      zIndex: {
        type: Number,
        default: 99
      },
      imageUrl: {
        type: String
      },
      fileType: {
        type: String,
        default: 'png'
      },
      quality: {
        type: Number,
        default: 1
      },
      width: {
        type: Number,
        default: 400
      },
      height: {
        type: Number,
        default: 400
      },
      minWidth: {
        type: Number,
        default: 200
      },
      maxWidth: {
        type: Number,
        default: 600
      },
      destWidth: Number,
      destHeight: Number,
      minHeight: {
        type: Number,
        default: 200
      },
      maxHeight: {
        type: Number,
        default: 600
      },
      isLockWidth: {
        type: Boolean,
        default: false
      },
      isLockHeight: {
        type: Boolean,
        default: false
      },
      isLockRatio: {
        type: Boolean,
        default: true
      },
      scaleRatio: {
        type: Number,
        default: 1
      },
      minRatio: {
        type: Number,
        default: 0.5
      },
      maxRatio: {
        type: Number,
        default: 2
      },
      isDisableScale: {
        type: Boolean,
        default: false
      },
      isDisableRotate: {
        type: Boolean,
        default: false
      },
      isLimitMove: {
        type: Boolean,
        default: false
      },
      isShowPhotoBtn: {
        type: Boolean,
        default: true
      },
      isShowRotateBtn: {
        type: Boolean,
        default: true
      },
      isShowConfirmBtn: {
        type: Boolean,
        default: true
      },
      isShowCancelBtn: {
        type: Boolean,
        default: true
      },
      rotateAngle: {
        type: Number,
        default: 90
      },
      source: {
        type: Object,
        default: () => ({
          album: '从相册中选择',
          camera: '拍照',
          // #ifdef MP-WEIXIN
          message: '从微信中选择'
          // #endif
        })
      }
    },
    data() {
      return {
        canvasWidth: 0,
        canvasHeight: 0,
        clipX: 0,
        clipY: 0,
        clipWidth: 0,
        clipHeight: 0,
        animation: false,
        imageWidth: 0,
        imageHeight: 0,
        imageTop: 0,
        imageLeft: 0,
        scale: 1,
        angle: 0,
        image: '',
        imageInit: false,
        sysinfo: {},
        throttleTimer: null,
        throttleFlag: true,
        timeClipCenter: null,
        flagClipTouch: false,
        flagEndTouch: false,
        clipStart: {},
        animationTimer: null,
        touchRelative: [{
          x: 0,
          y: 0
        }],
        hypotenuseLength: 0,
        ctx: null,
        canvasId: 'lime-clipper'
      };
    },
    computed: {
      // canvasId() {
      // 	return `l-clipper-${this._ ? this._.uid : this._uid}`
      // },
      clipStyle() {
        const {
          clipWidth,
          clipHeight,
          clipY,
          clipX,
          animation
        } = this
        return `
			width: ${clipWidth}px;
			height:${clipHeight}px;
			transition-property: ${animation ? '' : 'background'};
			left: ${clipX}px;
			top: ${clipY}px
			`
      },
      imageStyle() {
        const {
          imageWidth,
          imageHeight,
          imageLeft,
          imageTop,
          animation,
          scale,
          angle
        } = this
        return `
				width: ${imageWidth ? imageWidth + 'px' : 'auto'};
				height: ${imageHeight ? imageHeight + 'px' : 'auto'};
				transform: translate3d(${imageLeft - imageWidth / 2}px, ${imageTop - imageHeight / 2}px, 0) scale(${scale}) rotate(${angle}deg);
				transition-duration: ${animation ? 0.35 : 0}s
			`
      },
      clipSize() {
        const {
          clipWidth,
          clipHeight
        } = this;
        return {
          clipWidth,
          clipHeight
        };
      },
      clipPoint() {
        const {
          clipY,
          clipX
        } = this;
        return {
          clipY,
          clipX
        };
      }
    },
    watch: {
      value(val) {
        if (!val) {
          this.animation = 0
          this.angle = 0
        } else {
          if (this.imageUrl) {
            const {
              imageWidth,
              imageHeight,
              imageLeft,
              imageTop,
              scale,
              clipX,
              clipY,
              clipWidth,
              clipHeight,
              path
            } = cache?.[this.imageUrl] || {}
            if (path != this.image) {
              this.image = this.imageUrl;
            } else {
              this.setDiffData({
                imageWidth,
                imageHeight,
                imageLeft,
                imageTop,
                scale,
                clipX,
                clipY,
                clipWidth,
                clipHeight
              })
            }

          }

        }
      },
      imageUrl(url) {
        this.image = url
      },
      image: {
        handler: async function(url) {
          this.getImageInfo(url)
        },
        // immediate: true,
      },
      clipSize({
        widthVal,
        heightVal
      }) {
        let {
          minWidth,
          minHeight
        } = this;
        minWidth = minWidth / 2;
        minHeight = minHeight / 2;
        if (widthVal < minWidth) {
          this.setDiffData({
            clipWidth: minWidth
          })
        }
        if (heightVal < minHeight) {
          this.setDiffData({
            clipHeight: minHeight
          })
        }
        this.calcClipSize();
      },
      angle(val) {
        this.animation = this.imageInit;
        this.moveStop();
        const {
          isLimitMove
        } = this;
        if (isLimitMove && val % 90) {
          this.setDiffData({
            angle: Math.round(val / 90) * 90
          })
        }
        this.imgMarginDetectionScale();
      },
      animation(val) {
        clearTimeout(this.animationTimer);
        if (val) {
          let animationTimer = setTimeout(() => {
            this.setDiffData({
              animation: false
            })
          }, 260);
          this.setDiffData({
            animationTimer
          })
          this.animationTimer = animationTimer;
        }
      },
      isLimitMove(val) {
        if (val) {
          if (this.angle % 90) {
            this.setDiffData({
              angle: Math.round(this.angle / 90) * 90
            })
          }
          this.imgMarginDetectionScale();
        }
      },
      clipPoint() {
        this.cutDetectionPosition();
      },
      width(width, oWidth) {
        if (width !== oWidth) {
          this.setDiffData({
            clipWidth: uni.upx2px(width) //width / 2
          })
        }
      },
      height(height, oHeight) {
        if (height !== oHeight) {
          this.setDiffData({
            clipHeight: uni.upx2px(height) //height / 2
          })
        }
      }
    },
    mounted() {
      const sysinfo = uni.getSystemInfoSync();
      this.sysinfo = sysinfo;
      this.setClipInfo();
      this.image = this.imageUrl || this.image
      this.setClipCenter();
      this.calcClipSize();
      this.cutDetectionPosition();
    },
    methods: {
      setDiffData(data) {
        Object.keys(data).forEach(key => {
          if (this[key] !== data[key]) {
            this[key] = data[key];
          }
        });
      },
      getImageInfo(url) {
        if (!url) return;
        if (this.value) {
          uni.showLoading({
            title: '请稍候...',
            mask: true
          });
        }
        this.imageInit = false
        uni.getImageInfo({
          src: url,
          success: res => {
            if (['right', 'left'].includes(res.orientation)) {
              this.imgComputeSize(res.height, res.width);
            } else {
              this.imgComputeSize(res.width, res.height);
            }
            this.image = res.path;
            if (this.isLimitMove) {
              this.imgMarginDetectionScale();
              this.$emit('ready', res);
            }
            const {
              imageWidth,
              imageHeight,
              imageLeft,
              imageTop,
              scale,
              clipX,
              clipY,
              clipWidth,
              clipHeight
            } = this
            cache[url] = Object.assign(res, {
              imageWidth,
              imageHeight,
              imageLeft,
              imageTop,
              scale,
              clipX,
              clipY,
              clipWidth,
              clipHeight
            });
          },
          fail: (err) => {
            this.imgComputeSize();
            if (this.isLimitMove) {
              this.imgMarginDetectionScale();
            }
          }
        });

      },
      setClipInfo() {
        const {
          width,
          height,
          sysinfo,
          canvasId
        } = this;
        const clipWidth = width / 2;
        const clipHeight = height / 2;
        const clipY = (sysinfo.windowHeight - clipHeight) / 2; //剪切框x,y位置
        const clipX = (sysinfo.windowWidth - clipWidth) / 2;
        //初始化图片坐标在窗口中心
        const imageLeft = sysinfo.windowWidth / 2;
        const imageTop = sysinfo.windowHeight / 2;
        this.ctx = uni.createCanvasContext(canvasId, this);
        this.clipWidth = clipWidth;
        this.clipHeight = clipHeight;
        this.clipX = clipX;
        this.clipY = clipY;
        this.canvasHeight = clipHeight;
        this.canvasWidth = clipWidth;
        this.imageLeft = imageLeft;
        this.imageTop = imageTop;
      },
      setClipCenter() {
        const {
          sysInfo,
          clipHeight,
          clipWidth,
          imageTop,
          imageLeft
        } = this;
        let sys = sysInfo || uni.getSystemInfoSync();
        let clipY = (sys.windowHeight - clipHeight) * 0.5;
        let clipX = (sys.windowWidth - clipWidth) * 0.5;
        this.imageTop = imageTop - this.clipY + clipY;
        this.imageLeft = imageLeft - this.clipX + clipX;
        this.clipY = clipY;
        this.clipX = clipX;
      },
      calcClipSize() {
        const {
          clipHeight,
          clipWidth,
          sysinfo,
          clipX,
          clipY
        } = this;
        if (clipWidth > sysinfo.windowWidth) {
          this.setDiffData({
            clipWidth: sysinfo.windowWidth
          })
        } else if (clipWidth + clipX > sysinfo.windowWidth) {
          this.setDiffData({
            clipX: sysinfo.windowWidth - clipX
          })
        }
        if (clipHeight > sysinfo.windowHeight) {
          this.setDiffData({
            clipHeight: sysinfo.windowHeight
          })
        } else if (clipHeight + clipY > sysinfo.windowHeight) {
          this.clipY = sysinfo.windowHeight - clipY;
          this.setDiffData({
            clipY: sysinfo.windowHeight - clipY
          })
        }
      },
      cutDetectionPosition() {
        const {
          clipX,
          clipY,
          sysinfo,
          clipHeight,
          clipWidth
        } = this;
        let cutDetectionPositionTop = () => {
            if (clipY < 0) {
              this.setDiffData({
                clipY: 0
              })
            }
            if (clipY > sysinfo.windowHeight - clipHeight) {
              this.setDiffData({
                clipY: sysinfo.windowHeight - clipHeight
              })
            }
          },
          cutDetectionPositionLeft = () => {
            if (clipX < 0) {
              this.setDiffData({
                clipX: 0
              })
            }
            if (clipX > sysinfo.windowWidth - clipWidth) {
              this.setDiffData({
                clipX: sysinfo.windowWidth - clipWidth
              })
            }
          };
        if (clipY === null && clipX === null) {
          let newClipY = (sysinfo.windowHeight - clipHeight) * 0.5;
          let newClipX = (sysinfo.windowWidth - clipWidth) * 0.5;
          this.setDiffData({
            clipX: newClipX,
            clipY: newClipY
          })
        } else if (clipY !== null && clipX !== null) {
          cutDetectionPositionTop();
          cutDetectionPositionLeft();
        } else if (clipY !== null && clipX === null) {
          cutDetectionPositionTop();
          this.setDiffData({
            clipX: (sysinfo.windowWidth - clipWidth) / 2
          })
        } else if (clipY === null && clipX !== null) {
          cutDetectionPositionLeft();
          this.setDiffData({
            clipY: (sysinfo.windowHeight - clipHeight) / 2
          })
        }
      },
      imgComputeSize(width, height) {
        const {
          imageWidth,
          imageHeight
        } = calcImageSize(width, height, this);
        this.imageWidth = imageWidth;
        this.imageHeight = imageHeight;
      },
      imgMarginDetectionScale(scale) {
        if (!this.isLimitMove) return;
        const currentScale = calcImageScale(this, scale);
        this.imgMarginDetectionPosition(currentScale);
      },
      imgMarginDetectionPosition(scale) {
        if (!this.isLimitMove) return;
        const {
          scale: currentScale,
          left,
          top
        } = calcImageOffset(this, scale);
        this.setDiffData({
          imageLeft: left,
          imageTop: top,
          scale: currentScale
        })
      },
      throttle() {
        this.setDiffData({
          throttleFlag: true
        })
      },
      moveDuring() {
        clearTimeout(this.timeClipCenter);
      },
      moveStop() {
        clearTimeout(this.timeClipCenter);
        const timeClipCenter = setTimeout(() => {
          if (!this.animation) {
            this.setDiffData({
              imageInit: true,
              animation: true,
            })
          }
          this.setClipCenter();
        }, 800);
        this.setDiffData({
          timeClipCenter
        })
      },
      clipTouchStart(event) {
        // #ifdef H5
        event.preventDefault()
        // #endif
        if (!this.image) {
          uni.showToast({
            title: '请选择图片',
            icon: 'none'
          });
          return;
        }
        const currentX = event.touches[0].clientX;
        const currentY = event.touches[0].clientY;
        const {
          clipX,
          clipY,
          clipWidth,
          clipHeight
        } = this;
        const corner = determineDirection(clipX, clipY, clipWidth, clipHeight, currentX, currentY);
        this.moveDuring();
        if (!corner) {
          return
        }
        this.clipStart = {
          width: clipWidth,
          height: clipHeight,
          x: currentX,
          y: currentY,
          clipY,
          clipX,
          corner
        };
        this.flagClipTouch = true;
        this.flagEndTouch = true;
      },
      clipTouchMove(event) {
        // #ifdef H5
        event.stopPropagation()
        event.preventDefault()
        // #endif
        if (!this.image) {
          uni.showToast({
            title: '请选择图片',
            icon: 'none'
          });
          return;
        }
        // 只针对单指点击做处理
        if (event.touches.length !== 1) {
          return;

        }
        const {
          flagClipTouch,
          throttleFlag
        } = this;
        if (flagClipTouch && throttleFlag) {
          const {
            isLockRatio,
            isLockHeight,
            isLockWidth
          } = this;
          if (isLockRatio && (isLockWidth || isLockHeight)) return;
          this.setDiffData({
            throttleFlag: false
          })
          this.throttle();
          const clipData = clipTouchMoveOfCalculate(this, event);
          if (clipData) {
            const {
              width,
              height,
              clipX,
              clipY
            } = clipData;
            if (!isLockWidth && !isLockHeight) {
              this.setDiffData({
                clipWidth: width,
                clipHeight: height,
                clipX,
                clipY
              })
            } else if (!isLockWidth) {
              this.setDiffData({
                clipWidth: width,
                clipX
              })
            } else if (!isLockHeight) {
              this.setDiffData({
                clipHeight: height,
                clipY
              })
            }
            this.imgMarginDetectionScale();
          }

        }
      },
      clipTouchEnd() {
        this.moveStop();
        this.flagClipTouch = false;
      },
      imageTouchStart(e) {
        // #ifdef H5
        event.preventDefault()
        // #endif
        this.flagEndTouch = false;
        const {
          imageLeft,
          imageTop
        } = this;
        const clientXForLeft = e.touches[0].clientX;
        const clientYForLeft = e.touches[0].clientY;

        let touchRelative = [];
        if (e.touches.length === 1) {
          touchRelative[0] = {
            x: clientXForLeft - imageLeft,
            y: clientYForLeft - imageTop
          };
          this.touchRelative = touchRelative;
        } else {
          const clientXForRight = e.touches[1].clientX;
          const clientYForRight = e.touches[1].clientY;
          let width = Math.abs(clientXForLeft - clientXForRight);
          let height = Math.abs(clientYForLeft - clientYForRight);
          const hypotenuseLength = calcPythagoreanTheorem(width, height);

          touchRelative = [{
              x: clientXForLeft - imageLeft,
              y: clientYForLeft - imageTop
            },
            {
              x: clientXForRight - imageLeft,
              y: clientYForRight - imageTop
            }
          ];
          this.touchRelative = touchRelative;
          this.hypotenuseLength = hypotenuseLength;
        }
      },
      imageTouchMove(e) {
        // #ifdef H5
        event.preventDefault()
        // #endif
        const {
          flagEndTouch,
          throttleFlag
        } = this;
        if (flagEndTouch || !throttleFlag) return;
        const clientXForLeft = e.touches[0].clientX;
        const clientYForLeft = e.touches[0].clientY;
        this.setDiffData({
          throttleFlag: false
        })
        this.throttle();
        this.moveDuring();
        if (e.touches.length === 1) {
          const {
            left: imageLeft,
            top: imageTop
          } = imageTouchMoveOfCalcOffset(this, clientXForLeft, clientYForLeft);
          this.setDiffData({
            imageLeft,
            imageTop
          })
          this.imgMarginDetectionPosition();
        } else {
          const clientXForRight = e.touches[1].clientX;
          const clientYForRight = e.touches[1].clientY;
          let width = Math.abs(clientXForLeft - clientXForRight),
            height = Math.abs(clientYForLeft - clientYForRight),
            hypotenuse = calcPythagoreanTheorem(width, height),
            scale = this.scale * (hypotenuse / this.hypotenuseLength);
          if (this.isDisableScale) {

            scale = 1;
          } else {
            scale = scale <= this.minRatio ? this.minRatio : scale;
            scale = scale >= this.maxRatio ? this.maxRatio : scale;
            this.$emit('change', {
              width: this.imageWidth * scale,
              height: this.imageHeight * scale
            });
          }

          this.imgMarginDetectionScale(scale);
          this.hypotenuseLength = Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2));
          this.scale = scale;
        }
      },
      imageTouchEnd() {
        this.setDiffData({
          flagEndTouch: true
        })
        this.moveStop();
      },
      uploadImage() {
        const itemList = Object.entries(this.source)
        const sizeType = ['original', 'compressed']
        const success = ({
          tempFilePaths: a,
          tempFiles: b
        }) => {
          this.image = a ? a[0] : b[0].path
        };
        const _uploadImage = (type) => {
          if (type !== 'message') {
            uni.chooseImage({
              count: 1,
              sizeType,
              sourceType: [type],
              success
            });
          }
          // #ifdef MP-WEIXIN
          if (type == 'message') {
            wx.chooseMessageFile({
              count: 1,
              type: 'image',
              success
            })
          }
          // #endif
        }
        if (itemList.length > 1) {
          uni.showActionSheet({
            itemList: itemList.map(v => v[1]),
            success: ({
              tapIndex: i
            }) => {
              _uploadImage(itemList[i][0])
            }
          })
        } else {
          _uploadImage(itemList[0][0])
        }
      },
      imageReset() {
        const sys = this.sysinfo || uni.getSystemInfoSync();
        this.moveStop()
        this.scale = 1;
        this.angle = 0;

        this.imageTop = sys.windowHeight / 2;
        this.imageLeft = sys.windowWidth / 2;
      },
      imageLoad(e) {
        this.imageReset();
        uni.hideLoading();
        this.$emit('ready', e.detail);
      },
      rotate(event) {
        if (this.isDisableRotate) return;
        if (!this.image) {
          uni.showToast({
            title: '请选择图片',
            icon: 'none'
          });
          return;
        }
        const {
          rotateAngle
        } = this;
        const originAngle = this.angle
        const type = event.currentTarget.dataset.type;
        if (type === 'along') {
          this.angle = originAngle + rotateAngle
        } else {
          this.angle = originAngle - rotateAngle
        }
        this.$emit('rotate', this.angle);
      },
      confirm() {
        if (!this.image) {
          uni.showToast({
            title: '请选择图片',
            icon: 'none'
          });
          return;
        }
        uni.showLoading({
          title: '加载中'
        });
        const {
          canvasHeight,
          canvasWidth,
          clipHeight,
          clipWidth,
          scale,
          ctx,
          imageLeft,
          imageTop,
          clipX,
          clipY,
          angle,
          scaleRatio: dpr,
          image,
          quality,
          fileType,
          type: imageType,
          canvasId
        } = this;
        const draw = () => {
          const imageWidth = this.imageWidth * scale * dpr;
          const imageHeight = this.imageHeight * scale * dpr;
          const xpos = imageLeft - clipX;
          const ypos = imageTop - clipY;
          // const ctx = uni.createCanvasContext(canvasId, this);
          ctx.translate(xpos * dpr, ypos * dpr);
          ctx.rotate((angle * Math.PI) / 180);
          ctx.drawImage(image, -imageWidth / 2, -imageHeight / 2, imageWidth, imageHeight);
          ctx.draw(false, () => {
            const width = clipWidth * dpr
            const height = clipHeight * dpr
            let params = {
              x: 0,
              y: 0,
              width,
              height,
              destWidth: this.destWidth || width,
              destHeight: this.destHeight || height,
              canvasId,
              fileType,
              quality,
              success: (res) => {
                // 钉钉小程序
                data.url = res.tempFilePath || res.filePath;
                uni.hideLoading();
                this.$emit('success', data);
                this.$emit('input', false)
              },
              fail: (error) => {
                console.error('error', error)
                this.$emit('fail', error);
                this.$emit('input', false)
              }
            };

            let data = {
              url: '',
              width,
              height
            };
            uni.canvasToTempFilePath(params, this)
          });

        };

        if (canvasWidth !== clipWidth || canvasHeight !== clipHeight) {
          this.canvasWidth = clipWidth;
          this.canvasHeight = clipHeight;
          ctx.draw();
          this.$nextTick(() => {
            setTimeout(() => {
              draw();
            }, 100);
          })
        } else {
          draw();
        }
      },
      cancel() {
        this.$emit('cancel', false)
        this.$emit('input', false)
      },
    }
  };
</script>

<style lang="scss" scoped>
  @import './index';
</style>